ホームに戻る
出典 :
IDisposable インターフェイス (System) | Microsoft Learn C# の Dispose を正しく実装する - Qiita
関連 :
C#におけるusing virtual・abstract・sealed
目次 :

IDisposable と Dispose()

GC言語であるC#は非GC言語と異なり、任意のタイミングでオブジェクトを破棄してデストラクタを実行させることができない。
但し、IDisposable インタフェース(の Dispose() メソッド)を実装することで、当該オブジェクトの破棄に即した処理(主にリソースの解放)を実現することができる。
また、.NETの標準クラスライブラリの多くに IDisposable が実装されており、それらは Dispose() (によるリソース解放)を実行することができる。
( using を用いることで明示的なコールを省略できる。詳細はリンク先を参照。)

デストラクタとファイナライザ

C#はC++のデストラクタと同様、オブジェクトが破棄される際に実行したい処理を定義できる。
但し delete 演算子によりデストラクタを任意のタイミングで実行できるC++と異なり、C#でこの処理が実行されるのはGCが当該オブジェクトを破棄したタイミングとなる。
「GCがオブジェクトを破棄したタイミングで実行されるメソッド」をデストラクタと区別して「ファイナライザ」と呼ぶ。
このため、C#における終了処理はデストラクタではなくファイナライザである。(以前はデストラクタと呼称されており、デストラクタと表記する文献もなお存在する。)

IDisposable の意義

通常、オブジェクトが保持しているリソースの解放はファイナライザで行えば良い。
しかしファイナライザの実行タイミングはGCに依存し、リソースの解放に伴う処理が重い場合は、GCの負担が大きくなることがある。
オブジェクトの終了処理を一部肩代わりしてGCの負担を軽減することが、IDisposable 実装の目的である。
逆にGCにファイナライズを任せるのであれば IDisposable を実装する必要は無く、IDisposable が既に実装されているクラスオブジェクトについても Dispose() を実行する必要は無い。
( IDisposable は Dispose() が「実行できる」という意味しか持たず、Dispose() のコールが漏れた場合でもリソースリークが発生してはならない。)

IDisposable の定義と実装要件

IDisposable.cs
namespace System { [System.Runtime.InteropServices.ComVisible(true)] public interface IDisposable { void Dispose(); } }
IDisposable が求めているのは、void Dispose() を実装することのみである。

Dispose() の要件

IDisposable.cs の注釈では、以下が求められている。
  1. 安全に複数回呼び出し可能であること。
  2. インスタンスが保持しているリソースを解放すること。
  3. 必要ならば、基本クラスの Dispose() を呼び出すこと。
  4. Dispose() は当該クラスのファイナライズを抑制し、GCがファイナライズキュー上のオブジェクト数を減らすための手助けをする。
  5. 予期せぬ非常に重大なエラー( OutOfMemoryException など)を除いて、例外をスローすべきではない。
    理想的には、Dispose() を呼ぶことでオブジェクトに問題が発生しないようにするべきである。
1. にあるように、Dispose() を複数回コールした場合でもオブジェクトに問題が生じてはならない。
Dispose() は 2. にあるようにリソースの解放が主目的のため、これはひとつのリソースが重複して解放されることがあってはならないことを意味する。

リソースとは

ファイルストリームやデータベースコネクションなど、OSや外部DLLが管理するハンドルに紐づいたものを表す。
そのうち、C#のCLR(共通言語ランタイム)管理下にあるものを「マネージドリソース」、それ以外を「アンマネージドリソース」と呼ぶ。
具体的には、IDisposable インタフェースを実装したクラスのインスタンス( Dispose() をコールできる)がマネージドリソースである。
対してアンマネージドリソースはCLR外部から得たハンドルを指す。これは SafeHandle のサブクラスでラップすることでマネージドリソースに変換が可能である。
マネージドリソースは例外安全であるため、特段の理由が無い限りはマネージドリソースに変換して用いるのが良い。

IDisposable の実装例

アンマネージドリソースを持つ場合

基底クラスの実装
// IDisposable を実装する基底クラス public class MyBaseClass : IDisposable { private IntPtr mHandle; //< アンマネージドリソースのハンドル private Stream mStream; //< マネージドリソース private bool mDisposed = false; //< Dispose() が呼ばれたかどうか // Dispose() (公開メソッド) public void Dispose() { Dispose_Core(true); // GCによるファイナライズを抑制 GC.SuppressFinalize(this); } // Dispose() の主処理 protected virtual void Dispose_Core(bool disposing) { // 既に Dispose() が呼ばれている場合は処置不要 if (mDisposed) return; // マネージドリソースの解放 if (disposing) { mStream.Dispose(); } // アンマネージドリソースの解放 MyCloseHandle(mHandle); mHandle = IntPtr.Zero; // Dispose() 実行済 mDisposed = true; } // ファイナライザ ~MyBaseClass() { Dispose_Core(false); } // アンマネージドリソースの解放に伴う処理 protected static void MyCloseHandle(IntPtr handle) { : } }
派生クラスの実装
// MyBaseClass の派生クラス public class MySubClass : MyBaseClass { private IntPtr mHandle2; //< アンマネージドリソースのハンドル private Stream mStream2; //< マネージドリソース private bool mDisposed = false; //< Dispose() が呼ばれたかどうか // Dispose() の主処理(オーバーライド) protected override void Dispose_Core(bool disposing) { // 既に Dispose() が呼ばれている場合は処置不要 if (mDisposed) return; // マネージドリソースの解放 if (disposing) { mStream2.Dispose(); } // アンマネージドリソースの解放 MyCloseHandle(mHandle2); mHandle2 = IntPtr.Zero; // Dispose() 実行済 mDisposed = true; // 基底クラスの Dispose_Core() を呼ぶ base.Dispose_Core(disposing); } // ファイナライザで行うべき処理は基底クラスのファイナライザで実行しているため // 派生クラスのファイナライザは不要 }

解説

disposeパターンに基づく実装。
ここで、MyBaseClass では、Dispose() コール時とファイナライザ実行時の両方で Dispose_Core() をコールしている。
これは Dispose() がコールされずにGCがインスタンスを破棄した場合、ファイナライザで Dispose_Core() を行わなければアンマネージドリソースの解放漏れが発生することによる。
(マネージドリソースはCLRの管理下にあるため、Dispose() が呼ばれなかった場合でも解放漏れとなることはない。)
Dispose() とファイナライザの両方にアンマネージドリソースの解放処理を書くこともできるが、これはDRYの原則に反し、保守性が良くない。

マネージドリソースのみ持つ場合

基底クラスの実装
// IDisposable を実装する基底クラス public class MyBaseClass : IDisposable { private Stream mStream; //< マネージドリソース private bool mDisposed = false; //< Dispose() が呼ばれたかどうか // Dispose() (公開メソッド) public void Dispose() { Dispose_Core(true); // ファイナライザを持たないため、GC.SuppressFinalize() は不要 // GC.SuppressFinalize(this); } // Dispose() の主処理 protected virtual void Dispose_Core(bool disposing) { // 既に Dispose() が呼ばれている場合は処置不要 if (mDisposed) return; // マネージドリソースの解放 if (disposing) { mStream.Dispose(); } // Dispose() 実行済 mDisposed = true; } // アンマネージドリソースを持たないため、ファイナライザの定義は不要 }
派生クラスの実装
// MyBaseClass の派生クラス public class MySubClass : MyBaseClass { private Stream mStream2; //< マネージドリソース private bool mDisposed = false; //< Dispose() が呼ばれたかどうか // Dispose() の主処理(オーバーライド) protected override void Dispose_Core(bool disposing) { // 既に Dispose() が呼ばれている場合は処置不要 if (disposed) return; // マネージドリソースの解放 if (disposing) { mStream2.Dispose(); } // Dispose() 実行済 mDisposed = true; // 基底クラスの Dispose_Core() を呼ぶ base.Dispose_Core(disposing); } // アンマネージドリソースを持たないため、ファイナライザの定義は不要 }

解説

マネージドリソースはオブジェクト破棄の際に自動的に解放されるため、ファイナライザを記述する(マネージドリソースの Dispose() を呼ぶ)必要は無い。
ファイナライザ自体が存在しないため、Dispose() 中で GC.SuppressFinalize() を実行する必要も無い。

マネージドリソースのみ持つ場合(簡易版)

アンマネージドリソースを持たず、かつ継承を行わないのであれば更に簡略化できる。
ここでは、sealed 修飾を行うことで、派生クラスの作成を禁じている。
// IDisposable を実装するクラス(継承不可) public sealed class MyClass : IDisposable { private Stream mStream; //< マネージドリソース private bool mDisposed = false; //< Dispose() が呼ばれたかどうか // Dispose() (公開メソッド) public void Dispose() { // 既に Dispose() が呼ばれている場合は処置不要 if (mDisposed) return; // マネージドリソースの解放 mStream.Dispose(); // Dispose() 実行済 mDisposed = true; } }